using System.Diagnostics;
using System.Collections.Generic;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.nioneo.store
{


///
/// <summary> * An abstract representation of a dynamic store. The difference between a
/// * normal <seealso cref="AbstractStore"/> and a <CODE>AbstractDynamicStore</CODE> is
/// * that the size of a record/entry can be dynamic.
/// * <p>
/// * Instead of a fixed record this class uses blocks to store a record. If a
/// * record size is greater than the block size the record will use one or more
/// * blocks to store its data.
/// * <p>
/// * A dynamic store don't have a <seealso cref="IdGenerator"/> because the position of a
/// * record can't be calculated just by knowing the id. Instead one should use a
/// * <seealso cref="AbstractStore"/> and store the start block of the record located in the
/// * dynamic store. Note: This class makes use of an id generator internally for
/// * managing free and non free blocks.
/// * <p>
/// * Note, the first block of a dynamic store is reserved and contains information
/// * about the store. </summary>
/// 
	public abstract class AbstractDynamicStore : CommonAbstractStore
	{
///    
///     <summary> * Creates a new empty store. A factory method returning an implementation
///     * should make use of this method to initialize an empty store. Block size
///     * must be greater than zero. Not that the first block will be marked as
///     * reserved (contains info about the block size). There will be an overhead
///     * for each block of <CODE>13</CODE> bytes.
///     * <p>
///     * This method will create a empty store with descriptor returned by the
///     * <seealso cref="#getTypeAndVersionDescriptor()"/>. The internal id generator used
///     * by this store will also be created.
///     *  </summary>
///     * <param name="fileName">
///     *            The file name of the store that will be created </param>
///     * <param name="blockSize">
///     *            The number of bytes for each block </param>
///     * <param name="typeAndVersionDescriptor">
///     *            The type and version descriptor that identifies this store
///     *  </param>
///     * <exception cref="IOException">
///     *             If fileName is null or if file exists or illegal block size </exception>
///     
		protected internal static void createEmptyStore(string fileName, int baseBlockSize, string typeAndVersionDescriptor)
		{
			int blockSize = baseBlockSize;
		// sanity checks
			if (fileName == null)
			{
				throw new IllegalArgumentException("Null filename");
			}
			File file = new File(fileName);
			if (file.exists())
			{
				throw new IllegalStateException("Can't create store[" + fileName + "], file already exists");
			}
			if (blockSize < 1)
			{
				throw new IllegalArgumentException("Illegal block size[" + blockSize + "]");
			}
			blockSize += 13; // in_use(1)+length(4)+prev_block(4)+next_block(4)

		// write the header
			try
			{
				FileChannel channel = new FileOutputStream(fileName).getChannel();
				int endHeaderSize = blockSize + typeAndVersionDescriptor.getBytes().length;
				ByteBuffer buffer = ByteBuffer.allocate(endHeaderSize);
				buffer.putInt(blockSize);
				buffer.position(endHeaderSize - typeAndVersionDescriptor.Length);
				buffer.put(typeAndVersionDescriptor.getBytes()).flip();
				channel.write(buffer);
				channel.force(false);
				channel.Close();
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to create store " + fileName, e);
			}
			IdGeneratorImpl.createGenerator(fileName + ".id");
			IdGenerator idGenerator = new IdGeneratorImpl(fileName + ".id", 1);
			idGenerator.nextId(); // reserv first for blockSize
			idGenerator.Close();
		}

		private int blockSize;

		public AbstractDynamicStore<T1>(string fileName, Map<T1> config) : base(fileName, config)
		{
		}

		public AbstractDynamicStore(string fileName) : base(fileName)
		{
		}

///    
///     <summary> * Loads this store validating version and id generator. Also the block size
///     * is loaded (contained in first block) </summary>
///     
		protected internal override void loadStorage()
		{
			try
			{
				long fileSize = getFileChannel().size();
				string expectedVersion = getTypeAndVersionDescriptor();
				sbyte[] version = new sbyte[expectedVersion.getBytes().length];
				ByteBuffer buffer = ByteBuffer.wrap(version);
				getFileChannel().position(fileSize - version.Length);
				getFileChannel().read(buffer);
				buffer = ByteBuffer.allocate(4);
				getFileChannel().position(0);
				getFileChannel().read(buffer);
				buffer.flip();
				blockSize = buffer.getInt();
				if (blockSize <= 0)
				{
					throw new InvalidRecordException("Illegal block size: " + blockSize + " in " + getStorageFileName());
				}
				if (!expectedVersion.Equals(new string(version)))
				{
					if (!versionFound(new string(version)) && !isReadOnly())
					{
						setStoreNotOk();
					}
				}
				if ((fileSize - version.Length) % blockSize != 0 && !isReadOnly())
				{
					setStoreNotOk();
				}
				if (getStoreOk() && !isReadOnly())
				{
					getFileChannel().truncate(fileSize - version.Length);
				}
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to load storage " + getStorageFileName(), e);
			}
			try
			{
				if (!isReadOnly())
				{
					openIdGenerator();
				}
				else
				{
					openReadOnlyIdGenerator(getBlockSize());
				}
			}
			catch (InvalidIdGeneratorException e)
			{
				setStoreNotOk();
			}
			setWindowPool(new PersistenceWindowPool(getStorageFileName(), getBlockSize(), getFileChannel(), getMappedMem(), getIfMemoryMapped(), isReadOnly()));
		}

///    
///     <summary> * Returns the byte size of each block for this dynamic store
///     *  </summary>
///     * <returns> The block size of this store </returns>
///     
		public virtual int getBlockSize()
		{
			return blockSize;
		}

///    
///     <summary> * Returns next free block.
///     *  </summary>
///     * <returns> The next free block </returns>
///     * <exception cref="IOException">
///     *             If capacity exceeded or closed id generator </exception>
///     
		public virtual int nextBlockId()
		{
			return nextId();
		}

///    
///     <summary> * Makes a previously used block available again.
///     *  </summary>
///     * <param name="blockId">
///     *            The id of the block to free </param>
///     * <exception cref="IOException">
///     *             If id generator closed or illegal block id </exception>
///     
		public virtual void freeBlockId(int blockId)
		{
			freeId(blockId);
		}

	// in_use(byte)+prev_block(int)+nr_of_bytes(int)+next_block(int)
		private const int BLOCK_HEADER_SIZE = 1 + 4 + 4 + 4;

		public virtual void updateRecord(DynamicRecord record)
		{
			int blockId = record.getId();
			PersistenceWindow window = acquireWindow(blockId, OperationType.WRITE);
			try
			{
				Buffer buffer = window.getOffsettedBuffer(blockId);
				if (record.inUse())
				{
					Debug.Assert(record.getId() != record.getPrevBlock());
					buffer.put(Record.IN_USE.byteValue()).putInt(record.getPrevBlock()).putInt(record.Length).putInt(record.getNextBlock());
					if (!record.isLight())
					{
						if (!record.isCharData())
						{
							buffer.put(record.getData());
						}
						else
						{
							buffer.put(record.getDataAsChar());
						}
					}
				}
				else
				{
					buffer.put(Record.NOT_IN_USE.byteValue());
					if (!isInRecoveryMode())
					{
						freeBlockId(blockId);
					}
				}
			}
			finally
			{
				releaseWindow(window);
			}
		}

		public virtual Collection<DynamicRecord> allocateRecords(int startBlock, sbyte[] src)
		{
			Debug.Assert(getFileChannel() != null, "Store closed, null file channel");
			Debug.Assert(src != null, "Null src argument");
			List<DynamicRecord> recordList = new LinkedList<DynamicRecord>();
			int nextBlock = startBlock;
			int prevBlock = Record.NO_PREV_BLOCK.intValue();
			int srcOffset = 0;
			int dataSize = getBlockSize() - BLOCK_HEADER_SIZE;
			do
			{
				DynamicRecord record = new DynamicRecord(nextBlock);
				record.setCreated();
				record.setInUse(true);
				Debug.Assert(prevBlock != nextBlock);
				record.setPrevBlock(prevBlock);
				if (src.Length - srcOffset > dataSize)
				{
					sbyte[] data = new sbyte[dataSize];
					System.Array.Copy(src, srcOffset, data, 0, dataSize);
					record.setData(data);
					prevBlock = nextBlock;
					nextBlock = nextBlockId();
					record.setNextBlock(nextBlock);
					srcOffset += dataSize;
				}
				else
				{
					sbyte[] data = new sbyte[src.Length - srcOffset];
					System.Array.Copy(src, srcOffset, data, 0, data.Length);
					record.setData(data);
					nextBlock = Record.NO_NEXT_BLOCK.intValue();
					record.setNextBlock(nextBlock);
				}
				recordList.Add(record);
			}
			while (nextBlock != Record.NO_NEXT_BLOCK.intValue());
			return recordList;
		}

		public virtual Collection<DynamicRecord> allocateRecords(int startBlock, char[] src)
		{
			Debug.Assert(getFileChannel() != null, "Store closed, null file channel");
			Debug.Assert(src != null, "Null src argument");
			List<DynamicRecord> recordList = new LinkedList<DynamicRecord>();
			int nextBlock = startBlock;
			int prevBlock = Record.NO_PREV_BLOCK.intValue();
			int srcOffset = 0;
			int dataSize = getBlockSize() - BLOCK_HEADER_SIZE;
			do
			{
				DynamicRecord record = new DynamicRecord(nextBlock);
				record.setCreated();
				record.setInUse(true);
				Debug.Assert(prevBlock != nextBlock);
				record.setPrevBlock(prevBlock);
				if ((src.Length - srcOffset) * 2 > dataSize)
				{
					sbyte[] data = new sbyte[dataSize];
					CharBuffer charBuf = ByteBuffer.wrap(data).asCharBuffer();
					charBuf.put(src, srcOffset, dataSize / 2);
					record.setData(data);
					prevBlock = nextBlock;
					nextBlock = nextBlockId();
					record.setNextBlock(nextBlock);
					srcOffset += dataSize / 2;
				}
				else
				{
					if (srcOffset == 0)
					{
						record.setCharData(src);
					}
					else
					{
						sbyte[] data = new sbyte[(src.Length - srcOffset) * 2];
						CharBuffer charBuf = ByteBuffer.wrap(data).asCharBuffer();
						charBuf.put(src, srcOffset, src.Length - srcOffset);
						record.setData(data);
					}
					nextBlock = Record.NO_NEXT_BLOCK.intValue();
					record.setNextBlock(nextBlock);
				}
				recordList.Add(record);
			}
			while (nextBlock != Record.NO_NEXT_BLOCK.intValue());
			return recordList;
		}

		public virtual Collection<DynamicRecord> getLightRecords(int startBlockId)
		{
			List<DynamicRecord> recordList = new LinkedList<DynamicRecord>();
			int blockId = startBlockId;
			while (blockId != Record.NO_NEXT_BLOCK.intValue())
			{
				PersistenceWindow window = acquireWindow(blockId, OperationType.READ);
				try
				{
					DynamicRecord record = getLightRecord(blockId, window);
					recordList.Add(record);
					blockId = record.getNextBlock();
				}
				finally
				{
					releaseWindow(window);
				}
			}
			return recordList;
		}

		public virtual void makeHeavy(DynamicRecord record)
		{
			int blockId = record.getId();
			PersistenceWindow window = acquireWindow(blockId, OperationType.READ);
			try
			{
				Buffer buf = window.getBuffer();
			// NOTE: skip of header in offset
				int offset = (int)((blockId & 0xFFFFFFFFL) - buf.position()) * getBlockSize() + BLOCK_HEADER_SIZE;
				buf.setOffset(offset);
				sbyte[] bytes = new sbyte[record.Length];
				buf.get(bytes);
				record.setData(bytes);
			}
			finally
			{
				releaseWindow(window);
			}
		}

		private DynamicRecord getLightRecord(int blockId, PersistenceWindow window)
		{
			DynamicRecord record = new DynamicRecord(blockId);
			Buffer buffer = window.getOffsettedBuffer(blockId);
			sbyte inUse = buffer.get();
			if (inUse != Record.IN_USE.byteValue())
			{
				throw new InvalidRecordException("Block not inUse[" + inUse + "] blockId[" + blockId + "]");
			}
			record.setInUse(true);
			int prevBlock = buffer.getInt();
			record.setPrevBlock(prevBlock);
			int dataSize = getBlockSize() - BLOCK_HEADER_SIZE;
			int nrOfBytes = buffer.getInt();
			int nextBlock = buffer.getInt();
			if (nextBlock != Record.NO_NEXT_BLOCK.intValue() && nrOfBytes < dataSize || nrOfBytes > dataSize)
			{
				throw new InvalidRecordException("Next block set[" + nextBlock + "] current block illegal size[" + nrOfBytes + "/" + dataSize + "]");
			}
			record.Length = nrOfBytes;
			record.setNextBlock(nextBlock);
			record.setIsLight(true);
			return record;
		}

		private DynamicRecord getRecord(int blockId, PersistenceWindow window)
		{
			DynamicRecord record = new DynamicRecord(blockId);
			Buffer buffer = window.getOffsettedBuffer(blockId);
			sbyte inUse = buffer.get();
			if (inUse != Record.IN_USE.byteValue())
			{
				throw new InvalidRecordException("Not in use [" + inUse + "] blockId[" + blockId + "]");
			}
			record.setInUse(true);
			int prevBlock = buffer.getInt();
			record.setPrevBlock(prevBlock);
			int dataSize = getBlockSize() - BLOCK_HEADER_SIZE;
			int nrOfBytes = buffer.getInt();
			int nextBlock = buffer.getInt();
			if (nextBlock != Record.NO_NEXT_BLOCK.intValue() && nrOfBytes < dataSize || nrOfBytes > dataSize)
			{
				throw new InvalidRecordException("Next block set[" + nextBlock + "] current block illegal size[" + nrOfBytes + "/" + dataSize + "]");
			}
			record.Length = nrOfBytes;
			record.setNextBlock(nextBlock);
			sbyte[] byteArrayElement = new sbyte[nrOfBytes];
			buffer.get(byteArrayElement);
			record.setData(byteArrayElement);
			return record;
		}

		public virtual Collection<DynamicRecord> getRecords(int startBlockId)
		{
			List<DynamicRecord> recordList = new LinkedList<DynamicRecord>();
			int blockId = startBlockId;
			while (blockId != Record.NO_NEXT_BLOCK.intValue())
			{
				PersistenceWindow window = acquireWindow(blockId, OperationType.READ);
				try
				{
					DynamicRecord record = getRecord(blockId, window);
					recordList.Add(record);
					blockId = record.getNextBlock();
				}
				finally
				{
					releaseWindow(window);
				}
			}
			return recordList;
		}

///    
///     <summary> * Reads a <CODE>byte array</CODE> stored in this dynamic store using
///     * <CODE>blockId</CODE> as start block.
///     *  </summary>
///     * <param name="blockId">
///     *            The starting block id </param>
///     * <returns> The <CODE>byte array</CODE> stored </returns>
///     * <exception cref="IOException">
///     *             If unable to read the data </exception>
///     
		protected internal virtual sbyte[] @get(int blockId)
		{
			LinkedList<sbyte[]> byteArrayList = new LinkedList<sbyte[]>();
			PersistenceWindow window = acquireWindow(blockId, OperationType.READ);
			try
			{
				Buffer buffer = window.getOffsettedBuffer(blockId);
				sbyte inUse = buffer.get();
				if (inUse != Record.IN_USE.byteValue())
				{
					throw new InvalidRecordException("Not in use [" + inUse + "] blockId[" + blockId + "]");
				}
				int prevBlock = buffer.getInt();
				if (prevBlock != Record.NO_PREV_BLOCK.intValue())
				{
					throw new InvalidRecordException("Start[" + blockId + "] block has previous[" + prevBlock + "] block set");
				}
				int nextBlock = blockId;
				int dataSize = getBlockSize() - BLOCK_HEADER_SIZE;
				do
				{
					int nrOfBytes = buffer.getInt();
					prevBlock = nextBlock;
					nextBlock = buffer.getInt();
					if (nextBlock != Record.NO_NEXT_BLOCK.intValue() && nrOfBytes < dataSize || nrOfBytes > dataSize)
					{
						throw new InvalidRecordException("Next block set[" + nextBlock + "] current block illegal size[" + nrOfBytes + "/" + dataSize + "]");
					}
					sbyte[] byteArrayElement = new sbyte[nrOfBytes];
					buffer.get(byteArrayElement);
					byteArrayList.AddLast(byteArrayElement);
					if (nextBlock != Record.NO_NEXT_BLOCK.intValue())
					{
						releaseWindow(window);
						window = acquireWindow(nextBlock, OperationType.READ);
						buffer = window.getOffsettedBuffer(nextBlock);
						inUse = buffer.get();
						if (inUse != Record.IN_USE.byteValue())
						{
							throw new InvalidRecordException("Next block[" + nextBlock + "] not in use [" + inUse + "]");
						}
						if (buffer.getInt() != prevBlock)
						{
							throw new InvalidRecordException("Previous[" + prevBlock + "] block don't match");
						}
					}
				}
				while (nextBlock != Record.NO_NEXT_BLOCK.intValue());
			}
			finally
			{
				releaseWindow(window);
			}
			int totalSize = 0;
			LinkedList<sbyte[]>.Enumerator itr = byteArrayList.GetEnumerator();
			while (itr.MoveNext())
			{
				totalSize += itr.Current.length;
			}
			sbyte[] allBytes = new sbyte[totalSize];
			itr = byteArrayList.GetEnumerator();
			int index = 0;
			while (itr.MoveNext())
			{
				sbyte[] currentArray = itr.Current;
				System.Array.Copy(currentArray, 0, allBytes, index, currentArray.Length);
				index += currentArray.Length;
			}
			return allBytes;
		}

//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: private int findHighIdBackwards() throws IOException
		private int findHighIdBackwards()
		{
			FileChannel fileChannel = getFileChannel();
			int recordSize = getBlockSize();
			long fileSize = fileChannel.size();
			long highId = fileSize / recordSize;
			ByteBuffer byteBuffer = ByteBuffer.allocate(1);
			for (long i = highId; i > 0; i--)
			{
				fileChannel.position(i * recordSize);
				if (fileChannel.read(byteBuffer) > 0)
				{
					byteBuffer.flip();
					sbyte inUse = byteBuffer.get();
					byteBuffer.Clear();
					if (inUse != 0)
					{
						return (int) i;
					}
				}
			}
			return 0;
		}

///    
///     <summary> * Rebuilds the internal id generator keeping track of what blocks are free
///     * or taken.
///     *  </summary>
///     * <exception cref="IOException">
///     *             If unable to rebuild the id generator </exception>
///     
		protected internal override void rebuildIdGenerator()
		{
			if (getBlockSize() <= 0)
			{
				throw new InvalidRecordException("Illegal blockSize: " + getBlockSize());
			}
			logger.fine("Rebuilding id generator for[" + getStorageFileName() + "] ...");
			closeIdGenerator();
			File file = new File(getStorageFileName() + ".id");
			if (file.exists())
			{
				bool success = file.delete();
				Debug.Assert(success);
			}
			IdGeneratorImpl.createGenerator(getStorageFileName() + ".id");
			openIdGenerator();
			nextBlockId(); // reserved first block containing blockSize
			FileChannel fileChannel = getFileChannel();
			int highId = 0;
			long defraggedCount = 0;
			try
			{
				long fileSize = fileChannel.size();
				bool fullRebuild = true;
				if (getConfig() != null)
				{
					string mode = (string) getConfig().get("rebuild_idgenerators_fast");
					if (mode != null && mode.ToLower().Equals("true"))
					{
						fullRebuild = false;
						highId = findHighIdBackwards();
					}
				}
				ByteBuffer byteBuffer = ByteBuffer.wrap(new sbyte[1]);
				LinkedList<int> freeIdList = new LinkedList<int>();
				if (fullRebuild)
				{
					for (long i = 1; i * getBlockSize() < fileSize; i++)
					{
						fileChannel.position(i * getBlockSize());
						fileChannel.read(byteBuffer);
						byteBuffer.flip();
						sbyte inUse = byteBuffer.get();
						byteBuffer.flip();
						nextBlockId();
						if (inUse == Record.NOT_IN_USE.byteValue())
						{
							freeIdList.AddLast((int) i);
						}
						else
						{
							highId = (int) i;
							while (!freeIdList.Count == 0)
							{
								freeBlockId(freeIdList.RemoveFirst());
								defraggedCount++;
							}
						}
					}
				}
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to rebuild id generator " + getStorageFileName(), e);
			}
			setHighId(highId + 1);
			logger.fine("[" + getStorageFileName() + "] high id=" + getHighId() + " (defragged=" + defraggedCount + ")");
			closeIdGenerator();
			openIdGenerator();
		}


		protected internal virtual void updateHighId()
		{
			try
			{
				long highId = getFileChannel().size() / getBlockSize();
				base.setHighId(highId);
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException(e);
			}
		}
	}
}